# Copyright (C) 2025 China Telecom Quantum Group, QuantumCTek Co., Ltd.,
# Center for Excellence in Quantum Information and Quantum Physics.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Matplotlib-based quantum circuit drawer.

This module renders quantum circuits using Matplotlib with:
- Backend-independent font/line scaling.
- Optional square gate boxes to keep shapes consistent across backends.
- Folding support (multi-row layouts).
- Auto-fitting gate labels to box width.

Style is centralized; DPI and unit conversion are handled to ensure
visual consistency across different environments.

Notes:
    * Units in data coordinates are mapped to inches via ``UNIT_INCH`` to
      stabilize physical sizes regardless of DPI or rendering backend.
    * Chinese comments are provided sparingly in parentheses for key hints.
"""

import logging
from collections.abc import Sequence
from enum import IntEnum
from math import ceil

from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle, Arc, Circle

from cqlib.circuits.barrier import Barrier
from cqlib.circuits.bit import Bit
from cqlib.circuits.circuit import Circuit
from cqlib.circuits.instruction_data import InstructionData
from cqlib.circuits.gates import SWAP, CZ
from cqlib.circuits.gates.gate import ControlledGate
from cqlib.circuits.measure import Measure
from cqlib.circuits.qubit import Qubit
from cqlib.exceptions import CqlibError

from .base import BaseDrawer, BoxChar
from .style import Style

from ..utils import auto_close_inline_figures

logger = logging.getLogger('cqlib.vis')

# Map "data units" to inches so layout is backend-independent.
UNIT_INCH = 0.8361111

class ZOrder(IntEnum):
    """Z-order layers for circuit rendering.

    Ensures wires and connectors are drawn below gate boxes and texts.

    Attributes:
        LINE: Layer for wires/connectors.
        BOX: Layer for gate background rectangles.
        GATE: Layer for gate symbols and text.
    """
    LINE = 10
    BOX = 20
    GATE = 30


# pylint: disable=too-many-instance-attributes
class MatplotlibDrawer(BaseDrawer):
    """Quantum circuit visualizer powered by Matplotlib.

    Provides customizable styling, dynamic spacing, folding into rows, and
    auto-scaling text that fits inside gate boxes.

    Class Attributes:
        figure_styles (dict): Default figure-level styles and layout parameters.
    """

    figure_styles = {
        'figure_color': 'white',
        'axes_color': 'white',
        'show_axis': '0',
        'margin_top': 0.88,
        'margin_bottom': 0.11,
        'margin_left': 0.125,
        'margin_right': 0.9,
        'moment_width': 0.3,
        'gate_width': 1.4,
        'gate_height': 1.5,
        'font_size': 12,
        'text_color': 'black',
        'line_color': 'black',
        'line_width': 2.0,
        'dpi': 150,
    }

    # pylint: disable=too-many-arguments,too-many-positional-arguments
    def __init__(
            self,
            circuit: Circuit,
            qubit_order: list[int | Qubit] | None = None,
            figure_styles: dict[str, str | bool | float | tuple[float | int]] | None = None,
            gate_styles: dict[str, str | bool | float | tuple[float | int]] | None = None,
            title: str | None = None,
            fonts: str | list[str] | None = None,
            filename: str | None = None,
            style: str = 'default',
            initial_state: bool | None = None,
            reverse_bits: bool = False,
            fold: int = -1,
    ):
        """Initialize the drawer.

        Args:
            circuit (Circuit): Circuit object to visualize.
            qubit_order (list[int | Qubit] | None): Explicit display order for
                qubits. Defaults to the circuit's natural ordering. If ``reverse_bits``
                is ``True`` and ``qubit_order`` is ``None``, the display order is reversed.
            figure_styles (dict[str, str | bool | float | tuple[float | int]] | None):
                Figure-level style overrides (colors, margins, sizes).
            gate_styles (dict[str, str | bool | float | tuple[float | int]] | None):
                Gate-level style overrides (per gate type).
            title (str | None): Optional figure title.
            fonts (str | list[str] | None): Font family (or a list of families) to try.
            filename (str | None): If provided, the figure is saved to this path.
            style (str): Named style key used by ``Style`` for gate colors/lines.
            initial_state (bool | None): Whether to show ``|0>`` labels next to qubits.
            reverse_bits (bool): Whether to reverse the display order of bits.
            fold (int): Maximum number of moments per row before folding. Use ``-1`` to
                disable folding.

        Notes:
            The drawer computes a backend-derived scale from the live axes size,
            then multiplies by ``scale`` (if provided) to derive final font/line
            sizes. 
        """
        super().__init__(circuit, qubit_order)

        self._initial_state = initial_state
        self._reverse_bits = reverse_bits
        self.gate_style = Style(style, gate_styles)
        if figure_styles:
            self.figure_styles.update(figure_styles)

        if fonts and isinstance(fonts, str):
            fonts = [fonts]
        self._fonts = fonts
        self._title = title
        self._filename = filename
        self._fold = fold
        self._fig: Figure | None = None
        self._ax: Axes | None = None
        self._real_line_width = 0.0
        self._current_x = 0.0
        self._current_col_index = 0
        self._current_moment_index = 0
        self.column_width: list[float] = []

        sorted_qubits = self.sorted_qubits
        if reverse_bits and qubit_order is None:
            sorted_qubits = list(reversed(sorted_qubits))
        self.qubit_mapping = {q: i * 2 for i, q in enumerate(sorted_qubits)}
        self._display_qubits = sorted_qubits


    def drawer(self) -> Figure:
        """Render the complete circuit into a Matplotlib figure.

        This computes the layout (including folding into rows), establishes
        consistent row widths, and minimizes blank spaces. After figure/axes
        creation, it derives a backend scale from the axes' physical width and
        applies the final scale to fonts/line widths.

        Returns:
            Figure: Matplotlib figure holding the rendered circuit.

        Side Effects:
            If ``self._filename`` is set, the figure is saved with tight bbox.
            Inline figures may auto-close in certain environments to reduce
            output accumulation.
        """
        # Prepare layout lines (moments -> columns)
        lines = self._make_lines()

        # Initial state label
        initial_bit = r" $|0\rangle$" if self._initial_state else ""

        num_qubits = len(self._display_qubits)
        qubits_height = num_qubits * 2
        connect_height = 2.0

        num_moments = len(lines)
        if self._fold < 0 or num_moments <= self._fold:
            groups = [lines]
            row_widths = [self._real_line_width]
        else:
            groups = [lines[i:i + self._fold] for i in range(0, num_moments, self._fold)]
            row_widths = []
            for idx, group in enumerate(groups):
                if len(group) == 0:
                    rw = 0
                else:
                    rw = self.figure_styles['moment_width'] + sum(
                        m['width'] for m in group
                    ) + (len(group) - 1) * self.figure_styles['moment_width']
                    if idx < len(groups) - 1:
                        rw += self.figure_styles['moment_width']
                row_widths.append(rw)

        num_rows = len(groups)
        max_row_width = max(row_widths) if row_widths else 0
        total_height = num_rows * qubits_height + max(0, num_rows - 1) * connect_height

        # Setup figure and axes sized to max_row_width
        self._fig, self._ax = self.setup_figure(max_row_width + 1, total_height)

        # Trigger a draw to get accurate bbox; derive backend scale in inches.
        self._fig.canvas.draw()
        axes_w_in = self._ax.get_window_extent().transformed(self._fig.dpi_scale_trans.inverted()).width
        data_w_in = (max_row_width if max_row_width > 0 else 1.0) * UNIT_INCH

        fig_lw = self.figure_styles['line_width']
        fig_fs = self.figure_styles['font_size']

        y_base = 0.0
        line_color = self.figure_styles['line_color']
        line_lw = fig_lw

        for row_index, group in enumerate(groups):
            if row_index > 0:
                y_base += qubits_height + connect_height

            # Qubit labels (include |0> if requested)
            for i, qubit in enumerate(self._display_qubits):
                qubit_label = f"{qubit}{initial_bit}"
                self._ax.text(
                    -0.6,
                    y_base + i * 2,
                    qubit_label,
                    ha='right',
                    va='center',
                    fontsize=fig_fs,
                    color=self.figure_styles['text_color'],
                    zorder=ZOrder.GATE
                )

            # Horizontal wires for this row
            for i in range(num_qubits):
                qi_y = y_base + i * 2
                self._ax.hlines(
                    qi_y,
                    xmin=0,
                    xmax=max_row_width,
                    color=line_color,
                    linewidth=line_lw,
                    zorder=ZOrder.LINE
                )

            # Opening vertical line if not first row
            if row_index > 0:
                self._ax.vlines(
                    0,
                    ymin=y_base,
                    ymax=y_base + (num_qubits - 1) * 2,
                    color=line_color,
                    linewidth=line_lw * 1.5,
                    zorder=ZOrder.LINE
                )

            # Dynamic moment spacing within this row
            num_moments_in_row = len(group)
            if num_moments_in_row > 0:
                dynamic_moment_spacing = self.figure_styles['moment_width']
                self._current_x = dynamic_moment_spacing

                for i, moment in enumerate(group):
                    start = self._current_x
                    for column in moment['moment']:
                        column_width = column['width']
                        self._current_x += column_width / 2
                        self.draw_column(column['column'], y_offset=y_base)
                        self._current_x += column_width / 2

                    # Light background when a moment has multiple parallel columns
                    if len(moment['moment']) > 1:
                        self._ax.add_patch(
                            Rectangle(
                                (start - 0.1, y_base - 1),
                                self._current_x - start + 0.2,
                                qubits_height + 0.2,
                                facecolor='lightgray',
                                alpha=0.2,
                                zorder=0
                            )
                        )
                    if i < num_moments_in_row - 1:
                        self._current_x += dynamic_moment_spacing

            # Closing vertical line at the end of the row (if more rows follow)
            if row_index < num_rows - 1:
                self._current_x = max_row_width
                self._ax.vlines(
                    max_row_width,
                    ymin=y_base,
                    ymax=y_base + (num_qubits - 1) * 2,
                    color=line_color,
                    linewidth=line_lw * 1.5,
                    zorder=ZOrder.LINE
                )

        if self._filename:
            self._fig.savefig(
                self._filename,
                transparent=None,
                dpi=self._fig.dpi, 
                bbox_inches='tight'
            )

        auto_close_inline_figures(self._fig)
        return self._fig

    def _make_lines(self):
        """Transform moments to renderable columns and compute widths.

        Iterates over moments, expands to columns (with instruction widths),
        accumulates overall line width, and returns a structure consumable by
        the rendering loop.

        Returns:
            list[dict]: List of moment descriptors. Each dict contains ``width``
                (float) describing the total moment width and ``moment`` (list[dict])
                describing per-column instructions.
        """
        lines = []
        gate_h = self.figure_styles['gate_height']

        for moment in self.generate_moment():
            columns = []
            max_width = 0.0
            for column in self.moment_to_columns(moment):
                col_width = 0.0
                instructions = []
                for ins in column:
                    ins_width = self.figure_styles['gate_width']
                    if ins.instruction.params:
                        t = self._str_params(ins.instruction)
                        # Broaden width if parameter text is long
                        ins_width = max(
                            ins_width,
                            len(t) / 6 * self.figure_styles['gate_width']
                        )
                    instructions.append({'width': ins_width, 'instruction': ins})
                    col_width = max(ins_width, col_width)
                self._real_line_width += col_width
                columns.append({'width': col_width, 'column': instructions})
                max_width += col_width
            self._real_line_width += self.figure_styles['moment_width']
            lines.append({'width': max_width, 'moment': columns})
        return lines

    def setup_figure(self, width: float, height: float) -> tuple[Figure, Axes]:
        """Create and configure Matplotlib figure and axes.

        Uses ``UNIT_INCH`` to convert data units to inches, locks DPI, and sets
        an equal aspect ratio so square gates are not distorted.

        Args:
            width (float): Desired data width (in internal units).
            height (float): Desired data height (in internal units).

        Returns:
            tuple[Figure, Axes]: The created figure and axes.
        """
        import matplotlib as mpl
        dpi = int(self.figure_styles.get('dpi', 120))
        mpl.rcParams['figure.dpi'] = dpi
        mpl.rcParams['savefig.dpi'] = dpi

        fig_w_in = width  * UNIT_INCH
        fig_h_in = (height/2) * UNIT_INCH


        fig, ax = plt.subplots(
            figsize=(fig_w_in, fig_h_in),
            dpi=dpi,
            constrained_layout=False
        )
        fig.set_facecolor(self.figure_styles['figure_color'])
        ax.set_facecolor(self.figure_styles['axes_color'])
        plt.subplots_adjust(
            left=self.figure_styles['margin_left'],
            right=self.figure_styles['margin_right'],
            top=self.figure_styles['margin_top'],
            bottom=self.figure_styles['margin_bottom']
        )
        ax.set_xlim(-1, width - 1)
        ax.set_ylim(-1, height - 1)
        ax.invert_yaxis()

        # Keep square gates undistorted
        ax.set_aspect('equal', adjustable='box')

        if self._title:
            ax.set_title(self._title, loc='center', fontsize=plt.rcParams['font.size'])

        ax.axis(self.figure_styles['show_axis'] in [1, True, 'true', '1'])
        if self._fonts:
            plt.rcParams['font.sans-serif'] = self._fonts

        return fig, ax

    def draw_column(self, column: list[dict], y_offset: float = 0.0):
        """Render a single column of vertically aligned instructions.

        Args:
            column (list[dict]): Column entries ``{'width': float, 'instruction': InstructionData}``.
            y_offset (float): Vertical offset used when folding across multiple rows.

        Returns:
            None
        """
        for item in column:
            ins_width = item['width']
            ins = item['instruction']
            if len(ins.qubits) == 1:
                self._draw_single_gate(ins, ins_width, y_offset=y_offset)
            else:
                self._draw_multi_gate(ins, ins_width, y_offset=y_offset)

    def _draw_single_gate(
            self,
            instruction_data: InstructionData,
            width: float,
            gate_style=None,
            y_offset: float = 0.0
    ):
        """Draw a single-qubit gate (with optional parameter text).

        If the instruction is a barrier or measure, special renderers are used.

        Args:
            instruction_data (InstructionData): Instruction data with one qubit.
            width (float): Box width for this gate (data units).
            gate_style (dict | None): Optional gate-style override from ``Style``.
            y_offset (float): Vertical offset for folding.

        Returns:
            None
        """
        qubit_no = self.qubit_line_no(instruction_data.qubits[0]) + y_offset
        ins = instruction_data.instruction

        if isinstance(ins, Barrier):
            self._draw_barrier(instruction_data.qubits, y_offset=y_offset)
            return
        if gate_style:
            gs = gate_style
        else:
            gs = self.gate_style[ins.name]

        rect = Rectangle(
            (self._current_x - width / 2, qubit_no - self.figure_styles['gate_height'] / 2),
            width,
            self.figure_styles['gate_height'],
            fc=gs['background_color'],
            ec=gs['border_color'],
            lw=(gs.get('line_width', self.figure_styles['line_width'])),
            zorder=ZOrder.BOX
        )
        self._ax.add_patch(rect)

        if isinstance(ins, Measure):
            self._draw_measure(qubit_no)
            return

        current_y = qubit_no
        if ins.params:
            current_y = qubit_no - 0.3 * 0.65
            self._ax.text(
                self._current_x,
                qubit_no + 0.5 * 0.65,
                self._str_params(ins),
                ha='center',
                va='center',
                fontsize=(gs['font_size'] - 5),
                color=gs['text_color'],
                zorder=ZOrder.GATE
            )

        self._ax.text(
            self._current_x,
            current_y,
            ins.name,
            ha='center',
            va='center',
            fontsize=gs['font_size'],
            color=gs['text_color'],
            zorder=ZOrder.GATE
        )

    def _draw_measure(self, qubit_no: int):
        """Draw a measurement symbol (dial + arrow + 0/1 labels).

        Args:
            qubit_no (int): Y-position (data units) of the target qubit line.

        Returns:
            None
        """
        gs = self.gate_style['M']
        gh = self.figure_styles['gate_height']
        gw = self.figure_styles['gate_width']
        x, y = self._current_x, qubit_no

        self._ax.add_patch(Rectangle(
            (x - 0.5 * gw, y - 0.5 * gh),
            gw, gh,
            facecolor=gs['background_color'],
            edgecolor=gs['border_color'],
            linewidth=(gs.get('line_width', self.figure_styles['line_width'])),
            zorder=ZOrder.GATE,
        ))

        self._ax.add_patch(Arc(
            (x, y + 0.30 * gh),
            width=0.7 * gw,
            height=0.8 * gw,
            theta1=180,
            theta2=0,
            linewidth=2,
            color=gs['text_color'],
            zorder=ZOrder.GATE + 1,
        ))

        self._ax.arrow(
            x, y + 0.30 * gh,
            0.4 * gw, -0.4 * gh,
            head_width=gw / 10,
            head_length=gw / 10,
            length_includes_head=True,
            linewidth=2,
            color=gs['text_color'],
            zorder=ZOrder.GATE + 2,
        )

        self._ax.text(x - 0.2 * gw, y - 0.25 * gh, "0",
                      color=gs['text_color'], fontsize=gs['font_size'],
                      ha="center", va="center", zorder=ZOrder.GATE + 3)
        self._ax.text(x + 0.2 * gw, y - 0.25 * gh, "1",
                      color=gs['text_color'], fontsize=gs['font_size'],
                      ha="center", va="center", zorder=ZOrder.GATE + 3)

        self._ax.add_patch(Circle(
            (x, y + 0.30 * gh),
            radius=gw * 0.05,
            facecolor=gs['text_color'],
            edgecolor=gs['text_color'],
            lw=1,
            zorder=ZOrder.GATE + 3
        ))

    def _draw_multi_gate(self, instruction_data: InstructionData, width: float, y_offset: float = 0.0):
        """Draw a multi-qubit gate with connectors.

        Supports SWAP, CZ, Barrier, and general controlled gates.

        Args:
            instruction_data (InstructionData): Instruction with multiple target qubits.
            width (float): Column width (data units).
            y_offset (float): Vertical offset for folding.

        Returns:
            None
        """
        qs = instruction_data.qubits
        instruction = instruction_data.instruction
        if isinstance(instruction, SWAP):
            self._draw_swap(qs, y_offset=y_offset)
        elif isinstance(instruction, CZ):
            self._draw_cz(qs, y_offset=y_offset)
        elif isinstance(instruction, Barrier):
            self._draw_barrier(qs, y_offset=y_offset)
        elif isinstance(instruction, ControlledGate):
            self._draw_controlled_gate(instruction_data, width, y_offset=y_offset)

    def _draw_swap(self, qs: Sequence[Bit], y_offset: float = 0.0):
        """Draw a SWAP gate (two X-like crosses + vertical line).

        Args:
            qs (Sequence[Bit]): The two qubits involved in the swap.
            y_offset (float): Vertical offset for folding.

        Returns:
            None
        """
        gs = self.gate_style['SWAP']
        size_x = 0.2
        size_y = 0.2
        for q in qs:
            qubit_no = self.qubit_line_no(q) + y_offset
            self._ax.plot(
                [self._current_x - size_x, self._current_x + size_x],
                [qubit_no + size_y, qubit_no - size_y],
                color=gs['text_color'],
                linewidth=(gs.get('line_width', self.figure_styles['line_width'])),
                zorder=ZOrder.GATE,
            )
            self._ax.plot(
                [self._current_x - size_x, self._current_x + size_x],
                [qubit_no - size_y, qubit_no + size_y],
                color=gs['text_color'],
                linewidth=(gs.get('line_width', self.figure_styles['line_width'])),
                zorder=ZOrder.GATE,
            )

        self._ax.vlines(
            self._current_x,
            ymin=self.qubit_line_no(qs[0]) + y_offset,
            ymax=self.qubit_line_no(qs[1]) + y_offset,
            color=gs['line_color'],
            linewidth=(gs.get('line_width', self.figure_styles['line_width']))
        )

    def _draw_barrier(self, qs: Sequence[Bit], y_offset: float = 0.0):
        """Draw a barrier across specified qubits.

        Args:
            qs (Sequence[Bit]): Sequence of qubits to span.
            y_offset (float): Vertical offset for folding.

        Returns:
            None
        """
        gs = self.gate_style['B']

        if not qs:
            return

        # Compute vertical span
        y_positions = [self.qubit_line_no(q) + y_offset for q in qs]
        ymin = min(y_positions) - 1
        ymax = max(y_positions) + 1
        height = ymax - ymin

        # Semi-transparent rectangle background
        width = 0.2
        self._ax.add_patch(
            Rectangle(
                (self._current_x - width/2, ymin),
                width, height,
                color=gs['line_color'],
                alpha=0.15,
            )
        )

        # Dashed vertical line per qubit
        for q in qs:
            qubit_no = self.qubit_line_no(q) + y_offset
            self._ax.vlines(
                self._current_x,
                ymin=qubit_no - 1 + 0.1,
                ymax=qubit_no + 1 - 0.1,
                color=gs['line_color'],
                linestyles=(0, (8, 2)),
                linewidth=(gs.get('line_width', self.figure_styles['line_width'])),
                alpha=0.7,
            )

    def _draw_cz(self, qs: Sequence[Bit], y_offset: float = 0.0):
        """Draw a CZ gate as two dots connected vertically.

        Args:
            qs (Sequence[Bit]): Control and target qubits.
            y_offset (float): Vertical offset for folding.

        Returns:
            None
        """
        gs = self.gate_style['CZ']
        for q in qs:
            qubit_no = self.qubit_line_no(q) + y_offset
            self._ax.text(
                self._current_x,
                qubit_no,
                BoxChar.DOT.value,
                ha='center',
                va='center',
                color=gs['line_color'],
                fontsize=gs['font_size'],
                zorder=ZOrder.GATE,
            )
        self._ax.vlines(
            self._current_x,
            ymin=self.qubit_line_no(qs[0]) + y_offset,
            ymax=self.qubit_line_no(qs[1]) + y_offset,
            color=gs['line_color'],
            linewidth=(gs.get('line_width', self.figure_styles['line_width'])),
        )

    def _draw_controlled_gate(self, instruction_data: InstructionData, width: float, y_offset: float = 0.0):
        """Draw a general controlled gate with vertical connector.

        Control qubits are rendered as solid dots; the target is rendered as
        the underlying base gate.

        Args:
            instruction_data (InstructionData): Must wrap a ``ControlledGate``.
            width (float): Gate box width (data units) for the target base gate.
            y_offset (float): Vertical offset for folding.

        Raises:
            CqlibError: If the instruction is not a ``ControlledGate``.

        Returns:
            None
        """
        gs = self.gate_style[instruction_data.instruction.name]
        if not isinstance(instruction_data.instruction, ControlledGate):
            raise CqlibError(f"Invalid instruction type for controlled gate. {instruction_data}")
        qubit_nos = []
        for i, qubit in enumerate(instruction_data.qubits):
            qubit_no = self.qubit_line_no(qubit) + y_offset
            qubit_nos.append(qubit_no)
            if i in instruction_data.instruction.control_index:
                self._ax.text(
                    self._current_x,
                    qubit_no,
                    BoxChar.DOT.value,
                    ha='center',
                    va='center',
                    color=gs['line_color'],
                    fontsize=gs['font_size'],
                    zorder=ZOrder.GATE,
                )
            else:
                self._draw_single_gate(
                    InstructionData(instruction_data.instruction.base_gate, [qubit]),
                    width=width,
                    gate_style=gs,
                    y_offset=y_offset
                )
        self._ax.vlines(
            self._current_x,
            ymin=min(qubit_nos),
            ymax=max(qubit_nos),
            color=gs['line_color'],
            linewidth=(gs.get('line_width', self.figure_styles['line_width'])),
            zorder=ZOrder.LINE
        )

    @staticmethod
    def _is_inline_backend() -> bool:
        """Return True when the active Matplotlib backend is inline-compatible."""
        import matplotlib
        b = matplotlib.get_backend().lower()
        return ('backend_inline' in b) or ('nbagg' in b) or ('agg' in b)

# pylint: disable=too-many-arguments, too-many-positional-arguments
def draw_mpl(
        circuit: Circuit,
        title: str | None = None,
        filename: str | None = None,
        qubit_order: list[int | Qubit] | None = None,
        figure_styles: dict[str, str | bool | float | tuple[float | int]] | None = None,
        gate_styles: dict[str, str | bool | float | tuple[float | int]] | None = None,
        fonts: str | list[str] | None = None,
        style: str = 'default',
        scale: float | None = None,
        initial_state: bool = False,
        reverse_bits: bool = False,
        fold: int = 18,
) -> Figure:
    """Convenience API to render a circuit via Matplotlib.

    Args:
        circuit (Circuit): Quantum circuit to draw.
        title (str | None): Optional diagram title.
        filename (str | None): If provided, save the figure to this path.
        qubit_order (list[int | Qubit] | None): Custom display order for qubits.
        figure_styles (dict[str, str | bool | float | tuple[float | int]] | None):
            Figure-level style overrides (colors, margins, etc.).
        gate_styles (dict[str, str | bool | float | tuple[float | int]] | None):
            Gate-level style overrides.
        fonts (str | list[str] | None): Font family/families for text rendering.
        style (str): Style name consumed by ``Style``.
        scale (float | None): Reserved user scale multiplier (currently unused).
        initial_state (bool): Whether to show ``|0>`` labels for each qubit line.
        reverse_bits (bool): Whether to reverse the display order of bits.
        fold (int): Max number of moments per row before folding. Use ``-1`` to disable.

    Returns:
        Figure: The Matplotlib figure with the rendered circuit.
    """
    return MatplotlibDrawer(
        circuit,
        title=title,
        filename=filename,
        figure_styles=figure_styles,
        qubit_order=qubit_order,
        gate_styles=gate_styles,
        fonts=fonts,
        style=style,
        initial_state=initial_state,
        reverse_bits=reverse_bits,
        fold=fold,
    ).drawer()
